用代码逐帧画一支 MV:PDoomVideo 全流程拆解
看到那支 Clawd 手绘水彩风的《I'm Upping My P(doom)》MV,第一反应通常是「这是哪个生视频模型做的」。答案是:没有生视频模型。整支 156.6 秒的片子,每一帧都是 JavaScript 用 p5.brush 一笔一笔「画」出来的,再用无头 Chrome 截图、ffmpeg 拼成 MP4。作者 JohnHeibel 让 Claude Opus 5.5 在 Claude Code 里完成了全部代码,仓库里没有一行人写的代码1。
这篇笔记把仓库完整拆开:从一首歌怎么变成分镜、分镜怎么变成并行 Agent 的任务、每一帧怎么被画出来、最后怎么合成视频。最后一章给出基于起步模板 ClaudeAnimationBase2 自己复刻一支 MV 的操作步骤。
一、先建立整体认知
它不是「AI 生视频」,而是「AI 写动画程序」
两条路线的区别决定了后面所有设计:
| 维度 | 生视频模型(Sora / Seedance 等) | 本项目:代码逐帧绘制 |
|---|---|---|
| 产出物 | 像素视频 | 一段程序,运行后输出帧 |
| 可控性 | 靠提示词抽卡 | 每个角色、每个镜头、每个节拍都可精确修改 |
| 角色一致性 | 易漂移 | 同一个 clawd() 函数画出来,天然一致 |
| 卡点 | 难 | BPM 写进代码,动作天然踩拍 |
| 画风 | 取决于模型 | 取决于画笔库(p5.brush 水彩 + 墨线) |
| 代价 | 生成快 | 需要 Agent 写几千行代码,渲染按秒/帧计 |
所以这套方法的本质是:把 MV 当成一个软件工程项目,让 Claude 当导演 + 动画师 + 程序员。
成品规格
| 项 | 值 |
|---|---|
| 时长 | 156.6 s |
| 分辨率 / 帧率 | 1920×1080 / 24 fps(约 3760 帧) |
| 歌曲节奏 | 88 BPM,一拍 0.682 s,第一拍在 0.21 s |
| 结构 | 9 个章节(chapter),每章一个 JS 文件,共约 6600 行 |
| 技术栈 | p5.js 2.x + p5.brush 2.x + puppeteer-core + ffmpeg |
端到端流水线
歌曲 mp3 + 歌词时间轴 (lyrics.js)
│
▼
① STORYBOARD.md ← Opus 写:故事主线、角色表、逐句分镜(时间/歌词/画面/转场)
│
▼
② ANIMATION_GUIDE.md ← Opus 写:给子 Agent 的「导演简报」+ 绘图 API 手册
│
▼
③ 共享底座 src/*.js ← core(画笔/时间/镜头)、clawd(角色)、cast、props、timeline
│
▼
④ 9 个并行子 Agent ← 每人只改 src/ch/cXX_*.js 一个文件,画自己那一章
│ ↺ 自查回路:render.mjs --sheet 渲染联系表 → Agent 看图 → 改代码
▼
⑤ studio.html ← 浏览器页面,window.renderAt(t) 画出任意时刻的一帧
│
▼
⑥ render.mjs ← puppeteer 开多个无头 Chrome 页,并行逐帧截图 → out/frames/*.jpg
│
▼
⑦ ffmpeg ← 帧序列 + mp3 → out/pdoom.mp4
二、制作过程:两轮生成
仓库 README 说明,视频一共生成了两轮,都在 Claude Code 里完成1:
| 轮次 | 模型 / 推理强度 | 产物 | 效果 |
|---|---|---|---|
| 第一轮 | Opus 5.5(Medium) | legacy/flash-version.html,单文件约 1200 行 | Flash 风:扁平纯色块、粗描边、大量文字标签和气泡 |
| 第二轮 | Opus 5.5(xhigh) | 仓库其余全部内容 | 水彩笔刷风、每句歌词有独立画面和转场 |
作者给的方向极少,第一轮只有两条:
- 用 Clawd(Claude Code 吉祥物)的角色设计
- 给每句歌词有趣的画面和转场
第二轮在第一轮基础上追加了几条修改意见:改用 p5.brush 笔刷、每个场景要更有看点、每个场景都要转场到下一个、减少文字。第二轮耗时约 45 分钟。
两轮对比说明了一件事:推理强度和「给不给明确的美术约束」对成品影响极大。ClaudeAnimationBase 的 README 也提到,推理强度越高,模型做出的场景越「铺张」、越注重细节2。
三、第一步:分镜 STORYBOARD.md
分镜是整个项目的「单一事实来源」。所有子 Agent 都按它画,它决定了片子是「一部短片」还是「一堆片段拼贴」。
一个核心创意把全片串起来
PDoom 的分镜第一段不是镜头,而是一个 idea:整支 MV 是一场失控的舞台剧。开头是剧场红幕拉开,结尾是同一块幕布落下;中间 Clawd 从显示器里的小涂鸦长成星球大小的超级智能,最后一句 "Was it all for show?" 镜头拉远——原来一切都是道具,巨型 Clawd 是三个小 Clawd 套的戏服,蛇怪是木偶,全员谢幕。
这个设计同时解决了三个问题:开头结尾押韵、各种梗之间有了统一世界观、有一个反转收尾。
全片级的规则
分镜在逐镜头之前先写了一组「贯穿全片」的约束,这部分最值得抄:
| 约束 | 内容 |
|---|---|
| 每个镜头都要「发生点什么」 | 角色做事、东西坏掉、变形、追逐、坠落 |
| 少文字 | 笑点靠画面和表演;全片只用几个大拟声词(FOOM、BOOM、CHOMP、SLAM) |
| 场景而非卡片 | 每章发生在一个地点,镜头在里面移动;四段副歌都回到同一个舞台,逐次升级 |
| 有动机的转场 | 章节之间用笔刷擦除;章节内用动作带过剪辑点(咬到黑屏、火箭、坠落、穿过眼睛…) |
| 情绪要演出来 | 表情不能硬切:先挤眼、身体压扁拉伸、弹出小符号(汗滴、爱心、!),再睁开新表情 |
| 镜头永远在动 | 每个镜头都有推、摇、倾、甩、拉远或踩拍震动 |
| 色彩弧线 | 暖奶油灯光 → 天蓝 → 太空紫金 → 钢灰爵士蓝 → 机房青 → 警报红 → 回到剧场深红金 |
| 主角够大 | 副歌舞蹈镜头里主角约占画面高度 40% |
| 贯穿道具 | P(doom) 温度计:每段副歌 Clawd 用打气筒把数值往上打,8→34→61→86→99.9 |
逐镜头表格
每章一张表,时间精确到 0.1 秒,和歌词对齐:
## 1 · The Lab (1.5–23) · night indigo, lamp ochre, monitor teal
| Time | Lyric | Shot | Out |
| --------- | ---------------------------------- | ------------------------------------------------------------------- | ------------------------------- |
| 1.5–3.6 | I see sparks of AGI… | 昏暗实验室,越过研究员肩膀看显示器,屏幕里小 Clawd 在睡觉,然后睁眼 | 镜头持续推进 |
| 3.6–5.9 | …in your eyes | 推到 Clawd 脸上,眼睛变星星,火花像烟花一样喷出屏幕 | 踩拍切 |
| ... | | | |
| 17.9–22.5 | ChatGPT, please don't eat me alive | 追逐:Clawd 头顶像饭盒一样掀开露出牙齿…… | **CHOMP**:嘴合上盖住镜头,黑屏 |
四列的意义:Time 让子 Agent 知道边界,Lyric 是内容依据,Shot 写「画面 + 事件」,Out 写这个镜头怎么接到下一个。Out 列是保证整体性的关键——它强迫每个镜头都想清楚出口。
四、第二步:导演简报 ANIMATION_GUIDE.md
分镜写完后,Opus 要把 9 个章节分给并行子 Agent。子 Agent 是冷启动的,不知道前面讨论过什么,所以 Opus 写了一份简报,让每个子 Agent 读完就能独立开工。README 明确说这份文件是「Opus 写来给它并行运行的子 Agent 做简报的」1。
简报包含五块内容:
| 模块 | 内容 | 作用 |
|---|---|---|
| 章节契约 | 文件结构、chapter() 注册方式、只准改自己的文件 | 让并行不冲突 |
| 画布与布局 | 1920×1080;底部 y 975–1070 是卡拉 OK 字幕区,关键动作放在 y 960 以上 | 避免遮挡 |
| 绘图 API | paint、inkLine、镜头、时间、角色、道具的完整手册 | 子 Agent 不用读源码就能画 |
| 风格规则 | 水彩 + 墨线、柔和饱和色、踩拍动作、一个焦点动作、性能预算 | 统一画风 |
| 自查方法 | 用 render.mjs --sheet 渲染联系表,用 Read 工具看图,反复迭代 | 让 Agent 能「看见」自己的作品 |
并行开发的隔离规则
这几条规则让 9 个 Agent 同时写代码而互不踩踏:
- 每章一个文件
src/ch/cXX_name.js,整个文件包在 IIFE 里,私有 helper 随便起名不会冲突。 - 只能改自己的章节文件。共享文件(core / clawd / cast / props / timeline / lyrics / studio.html / render.mjs)发现 bug 只报告,不修改;缺 helper 就在自己的 IIFE 里私有实现。
- 需要被终章谢幕复用的客串角色(蛇怪、shoggoth、龙猫等),挂到全局
CAST对象上导出。 - 渲染用真实 GPU,多个 Agent 可以同时渲染检查。
// src/ch/c03_takeoff.js
(() => {
const gym = t => { ... }; // 私有 helper,不会和其他章节冲突
function stable(t, lt, dur) { ... } // 一个镜头
function singularity(t, lt, dur) { ... }
chapter('takeoff', 38.5, 59.0, [[38.5, stable], [41.5, singularity], ...]);
CAST.basilisk = (x, y, s, t, o = {}) => { ... }; // 只导出终章要复用的角色
})();
五、第三步:渲染引擎的核心机制
这一章是整套方案能跑起来的技术底座,也是自己复刻时最需要理解的部分。
1. 每一帧都是时间 t 的纯函数
这是最重要的一条约束。渲染时多个 Chrome 页面并行、乱序地画帧,所以:
- 镜头函数签名是
fn(t, lt, dur):t是歌曲时间,lt是镜头内时间,dur是镜头长度 - 每次调用必须画完整一帧,包括背景
- 不能有跨帧状态、计数器、逐帧积分的物理,禁止
Math.random() - 一切位置、角度、颜色都用
t的闭式表达式算出来
// 一个镜头:t 相同,画面就必须完全相同
function labOver(t, lt, dur) {
const e = ease(lt / dur);
camBegin(lerp(960, 1030, e), lerp(540, 470, e), lerp(1, 1.32, e), 0); // 整个镜头缓慢推进
wall();
desk(700);
const s = monitor(1040, 455, 560, 450, { glow: 1 });
const md = mood(t, [[1.5, 'closed', 'zzz'], [B(4), 'normal', '!'], [B(5) - .15, 'look']]); // 睡 → 惊醒 → 张望
// ...
camEnd();
}
随机性分两种:
| 需求 | 用法 | 说明 |
|---|---|---|
| 固定随机(星星位置、书本高度) | hash(i) | 同一个 i 永远返回同一个值 |
| 手绘抖动(线条「沸腾」) | jit(a) / random() | 每秒重置种子 12 次(BOIL = 12),线条像手绘动画一样微微颤动 |
「沸腾(boil)」是手绘感的关键来源:draw() 开头执行 randomSeed(1000 + Math.floor(T * BOIL)),同一个 1/12 秒内的帧画出完全相同的抖动,下一个 1/12 秒换一组,效果等同于传统动画「一拍二」重描。
2. 时间轴:章节 → 镜头
timeline.js 维护一个章节注册表。drawWorld(t) 每帧做的事:
找到 t 所在的 chapter
→ 在章节的 shots 里找到当前 shot,调用 fn(t, t - t0, end - t0)
→ flushLetters() 把排队的文字合成进画面
→ 如果本镜头没画温度计,在副歌窗口画角落小温度计
→ 如果 t 靠近章节分界点(1.5 / 38.5 / 73 / 109.4 s),画笔刷擦除转场
→ karaoke(t) 画底部卡拉 OK 字幕条
注意:转场、字幕、角落温度计都是 timeline 自动叠加的,子 Agent 只管画自己的镜头内容。
3. 音乐同步:一切踩在拍子上
const BPM = 88, BEAT = 60 / BPM, OFF = 0.21; // 第一拍在 0.21 s
const bpOf = t => (t - OFF) / BEAT; // 当前拍位置(浮点)
const beatN = t => Math.floor(bpOf(t)); // 第几拍
const pulse = (t, k = 6) => Math.exp(-frac(bpOf(t)) * k); // 每拍瞬间为 1,然后衰减
pulse(t) 是最常用的:乘到缩放、位移、震动幅度上,东西就会「每拍一跳」。舞蹈函数 move(style, t) 也基于 bpOf(t),所以所有角色天然踩拍。分镜里的关键动作也都写成拍点时间,例如第一章用 B(n) = OFF + n * BEAT 计算第 n 拍的时刻,CHOMP 合嘴就落在 B(33)。
4. 绘画封装 paint()
所有图形都走 paint(pts, o),一次调用 = 一个形状,可叠加四层:
| 选项 | 效果 | 用途 |
|---|---|---|
wash | 平涂纯色 | 角色、道具等必须清晰的东西 |
fill + bleed/tex/border | 水彩填充,边缘晕染、有颜料纹理 | 背景、光晕、阴影 |
hatch | 排线 | 干笔质感,少量使用 |
ink + sw | 墨线描边(一条连续变粗细的笔触) | 轮廓;ink: null 表示不描边 |
形状用点列表描述:rectPts、ellPts、rrPts(圆角矩形)、starPts、heartPts,都带抖动参数。禁止直接用 p5 的 rect() / ellipse() / fill()——那会画出 2000 年代 Flash 的感觉,第一轮失败正是这个原因。
5. 镜头、全屏特效和文字
| API | 作用 |
|---|---|
camBegin(cx, cy, zoom, rot) / camEnd() | 把世界坐标点放到屏幕中心,实现推拉摇移;只能一层,必须成对 |
shakeXY(t, amt) | 确定性震屏(按 24fps 取 hash) |
flash(k) | 全屏闪白 |
iris(cx, cy, r) / irisShape(pts) | 圆形或任意形状的遮罩转场(例如嘴形、心形、钥匙孔) |
letter() / sfx() | 文字和漫画拟声词,先排队,flushLetters() 时合成 |
6. 角色系统
clawd(x, y, u, o) 画一个 Clawd,u 是尺寸单位(身体 10u × 8u)。所有能力都通过参数组合:
- 姿态:
dy、sq(压扁拉伸)、rot、flip、aL/aR(手臂角度)、walk - 表情:
eyes(normal、spark、heart、swirl、shades…)、mouth、blush - 帽子:party、crown、halo、fedora、cat…
- 表情符号:
emote(sweat、heart、!、zzz…) - 钩子:
draw在身体坐标系画配件,armL/armR在手尖画手持道具,道具自动跟随手臂 lid:头顶像饭盒一样掀开露出牙齿(CHOMP 梗)
两个关键辅助函数让表演不僵硬:
// 情绪切换:自动做「眯眼 → 压扁 → 弹出符号 → 睁开新表情」,不会硬切
const md = mood(t, [[0, 'normal'], [2.1, 'scared', 'sweat'], [3.4, 'happy', 'heart']]);
clawd(960, 860, 24, { ...md, mouth: 'smile' });
// 舞蹈:返回踩拍的姿态偏移
dancer(960, 860, 40, 'bounce', t, { eyes: 'happy' });
7. 一帧的合成顺序
WEBGL 画布(p5.brush)
1. 纸张底纹(预生成,一次性)
2. drawWorld(t):镜头内容 → 文字 → 角落温度计 → 转场 → 字幕底条
│
▼
2D 输出画布 #out
3. 拷贝 WEBGL 画面
4. 叠文字
5. multiply 叠加纸纹颗粒 + 暗角(颜料「渗进」纸里的感觉)
6. 画卡拉 OK 字幕(逐词变色)
│
▼
window.renderAt(t) → toDataURL() → 交给 puppeteer
studio.html 同时是开发工具:直接用 Chrome 打开,底部有时间滑块可以拖动预览任意时刻;URL 带 ?render 时隐藏开发 UI 供无头渲染使用。
六、第四步:Agent 的自查回路
模型读代码看不出动作好不好,所以简报要求每个子 Agent 渲染成图、用 Read 工具看图、再改代码:
# 联系表:一张图里放多个时刻的缩略图,同时打印每帧耗时
node render.mjs --sheet=38.6,39.3,40.1,40.9,41.6,42.3 --cols=3 --w=640 --out=out/check/c03_a.jpg
# 全分辨率单帧
node render.mjs --stills=39.5,43.2 --out=out/check/c03_full
检查清单:
- 每个镜头的首帧、尾帧和中间几帧
- 关键动作附近每 0.1 s 取一帧,看动作连不连贯
- 本章的入场和出场转场
- 重要内容有没有被底部字幕条挡住
- 性能:每帧 ≤ 2.5 s,最多不超过 4 s;成本主要来自
fill形状和笔画数量,几百个没问题,几千个不行
这个回路是「代码生成动画」能达到可看水平的关键:模型有了眼睛。
七、第五步:出片
npm install
node render.mjs --frames=0:156.6 --workers=4 # 4 个页面并行画帧 → out/frames/f00000.jpg ...
node render.mjs --encode --out=out/pdoom.mp4 # 帧序列 + assets/pdoom.mp3 → MP4
render.mjs 的几个设计细节:
| 细节 | 做法 | 目的 |
|---|---|---|
| 并行 | 开 N 个 page,共享一个待渲染队列,每页取下一个帧号 | 利用纯函数特性乱序渲染 |
| 可续跑 | 启动时跳过已存在且 > 1KB 的帧文件 | 中断后继续,不用重来 |
| 原子写入 | 先写 .tmp 再 rename | 中断不会留下半张图 |
| 编码参数 | libx264 -crf 17 -preset slow -pix_fmt yuv420p,音频 aac 192k,-shortest | 高画质、兼容主流播放器 |
| 快速预览 | --clip=0:6 通过管道边画边送进 ffmpeg | 调试短片段 |
八、自己动手:基于 ClaudeAnimationBase 复刻一支 MV
PDoomVideo 是为这首歌定制的,很多地方写死了(时长 156.6、Chrome 路径是 Windows 的 C:/Program Files/...、GPU 参数是 d3d11)。作者后来把整条管线抽成了通用起步模板 ClaudeAnimationBase,自己做 MV 应该从它开始。
本章针对「已经有一首歌」的情况,步骤 2 的音乐准备必须人工完成。只想快速出片、不想找音乐时,改用 TTS 旁白 + 程序化 BGM 的方案,见 不找配乐也能出片:TTS 驱动的手绘动画短片手把手教程。
两个仓库的差异
| 维度 | PDoomVideo | ClaudeAnimationBase |
|---|---|---|
| 定位 | 一支成片的源码 | 通用起步模板 |
| 结构 | 9 个 chapter 文件 + chapter() | src/scenes/*.js + shots() |
| 配置 | 常量写死在 core.js | src/config.js:PROJECT = { duration, bpm, offset, audio } |
| Chrome 查找 | 写死 Windows 路径 | 自动查找 Win / macOS / Linux 常见路径,或 CHROME_PATH |
| GPU 参数 | 固定 --use-angle=d3d11 | 按平台选择 d3d11 / metal / angle |
| Clawd | 基础表情 + mood() | 31 种情绪 feel() / emotions()、5 个转身视角 turn()、jump、stroll、spring 等表演函数 |
| 防抖 | 全局 boil 种子 | boilSeed(key) 按元素分别设种子,避免运动物体导致静止物体乱抖 |
| 光效 | 无 | glow() 加色光,解决「黄光叠蓝底变绿」的颜料混色问题 |
| 自查工具 | --sheet、--stills | 额外有 --strip(一段时间内逐帧)和 --crop(局部放大看细节) |
| 指南 | 章节分工简报 | 完整规则:三大目标 + 7 条规则 + 动画原理 + 工作流 + 常见失败清单 |
| 歌词字幕 | 有卡拉 OK 条 | 默认不上字幕:歌词要「演出来」而不是写出来 |
前置条件
| 依赖 | 说明 |
|---|---|
| Node.js | 跑 render.mjs |
| Google Chrome | puppeteer-core 不自带浏览器,调用本机 Chrome |
| ffmpeg | 编码 MP4 |
| 独立 GPU(推荐) | p5.brush 水彩填充依赖 WebGL;核显会慢到每帧数秒,作者建议此时让模型少用水彩 fill |
| Claude Code + Opus 5.5 | 作者所有测试视频用 xhigh 推理生成2 |
步骤 1:跑通模板
git clone https://github.com/JohnHeibel/ClaudeAnimationBase my-mv
cd my-mv
npm install
node render.mjs --sheet=0.5,3,6,9 --cols=2 --w=480 --out=out/check/a.jpg # 先出一张联系表验证环境
node render.mjs --clip --out=out/video.mp4 # 渲染 11 秒 demo
用 Chrome 打开 studio.html 可以拖动预览;加 ?loop=emotions 或 ?loop=views 查看角色设定图。
步骤 2:准备音乐素材
MV 相比普通短片多出来的工作都在这一步,而且必须人来做准(模型听不见音乐):
- BPM:用任意测速工具或 DAW 测出歌曲 BPM。
- 第一拍偏移 offset:第一个重拍出现的秒数(PDoom 是 0.21 s)。offset 错了,所有舞蹈都会「抢拍」或「拖拍」。
- 歌词时间轴:每句的开始、结束秒数。可以从带字幕的视频里抄,或者用 Whisper 之类的工具出时间戳再人工校对。格式照抄 PDoom:
// 歌词时间轴:[开始, 结束, 歌词]
const LY = [
[1.5, 5.9, "I see sparks of AGI in your eyes"],
[6.0, 7.9, "Your circuits make me nervous,"],
// ...
];
- 段落结构:标出前奏、主歌、副歌、间奏、尾奏的时间范围。副歌重复是设计「同一场景逐次升级」的天然素材。
然后改配置、放音频:
// src/config.js
const PROJECT = { duration: 156.6, bpm: 88, offset: 0.21, audio: 'assets/song.mp3' };
步骤 3:让 Claude 先出分镜,不写代码
最小提示词(模板 README 里的示例)2:
Read ANIMATION_GUIDE.md, then make a 15-second video of Clawd trying to catch a butterfly.
做 MV 建议显式要求「先分镜,确认后再写代码」,并把音乐信息一次给全:
读 ANIMATION_GUIDE.md。我要为 assets/song.mp3 做一支完整 MV。
- 歌曲 BPM=88,第一拍 0.21s,时长 156.6s,歌词时间轴见 src/lyrics.js
- 段落:前奏 0–1.5,主歌1 1.5–23,副歌1 23–38.5,……
- 主角用 Clawd;每句歌词要有独立、有趣的画面,每个镜头都要转场到下一个
- 用 p5.brush 笔刷,不要文字标签,歌词含义靠表演而不是字幕
先只写 STORYBOARD.md:一个贯穿全片的核心创意、角色表、色彩弧线、贯穿道具、
按章节分表(时间 / 歌词 / 画面与事件 / 出场转场),每个镜头列出观众需要看懂的 reads 及其时间。
写完停下来给我看,不要写代码。
拿到分镜后对照这张清单审一遍(来自模板指南):
- 每个镜头都有「事件」吗?首帧和尾帧之间有变化吗?
- 每个 read 都有足够时间被看懂,而且重要的 read 没有重叠吗?
- 每个接缝都有转场吗?
- 有没有文字?
- 结尾和开头押韵吗?
步骤 4:共享底座 + 并行子 Agent
分镜确认后,复刻 PDoom 的分工方式:
按 STORYBOARD.md 实现:
1. 先由你自己补齐共享层:新角色、贯穿道具、场景道具放进 src/ 的共享文件,并补一个 chapter 级的时间轴(按章节时间分发镜头)。
2. 写一份给子 Agent 的简报(章节契约、画布布局、可用 API、风格规则、自查命令),追加到 ANIMATION_GUIDE.md 或单独成文。
3. 按章节拆分,每章一个 src/ch/cXX_name.js,开子 Agent 并行实现。每个子 Agent 只能改自己的文件,共享文件有 bug 只报告不修改。
4. 每个子 Agent 必须用 render.mjs --sheet / --strip 渲染并查看自己的镜头,迭代到满意为止。
为什么要先建共享层再并行:角色、道具、转场、时间轴如果让每个子 Agent 各写一份,画风和比例一定不一致;共享层先定死,并行时每个人只负责「演戏」。
步骤 5:总验收与出片
所有章节完成后,自己(或让主 Agent)做一次全片验收:
# 每章抽首、中、尾帧,看整体节奏和色彩弧线
node render.mjs --sheet=0.5,10,22.8,23.2,30,38.4,38.6,50 --cols=4 --w=480 --out=out/check/all_a.jpg
# 转场接缝逐帧检查
node render.mjs --strip=38.2:38.8 --cols=6 --w=320 --out=out/check/seam_38.jpg
# 出片
node render.mjs --frames --workers=4
node render.mjs --encode --audio=assets/song.mp3 --out=out/video.mp4 # --encode 不读 PROJECT.audio,必须显式传
只有 --clip(一条命令直出 MP4)会自动读取 PROJECT.audio。
九、边界、代价与常见坑
这套方法的代价
- 渲染慢:水彩
fill是性能大头。PDoom 的预算是每帧 ≤ 2.5 s,3760 帧单线程就是 2.5 小时,所以需要多 worker 并行和续跑。实际速度取决于 GPU:在 Apple Silicon Mac 上实测,PDoom 成片约 170~400 ms/帧,Base 模板 demo 约 80~200 ms/帧,远低于预算。 - Token 与时间:第二轮约 45 分钟;Donald Jewkes 在此基础上接入 fal 生图、Seedance 生视频作为底层素材、再用 JS 重绘覆盖,耗时约 12 小时。
- 音乐理解靠人:BPM、offset、歌词时间轴、段落划分,模型都拿不到,必须人提供准确数据。
- 画风上限受画笔库限制:p5.brush 擅长绘本水彩风,写实风格不适合走这条路。
常见坑
| 坑 | 表现 | 解决 |
|---|---|---|
用了 Math.random() 或跨帧状态 | 并行渲染后画面闪烁、跳变 | 一律用 hash(i) 和 t 的闭式计算 |
| 静止物体乱抖 | 前面有运动物体消耗了不同数量的随机数,导致后面全部重新「沸腾」 | 每个独立元素前调用 boilSeed(key)(Base 模板提供) |
| 光晕发脏发绿 | p5.brush 按颜料混色,黄叠蓝变绿 | 发光物用 glow()(加色),或用满不透明度 wash 覆盖 |
| 画面文字太多 | 招牌、标签、气泡里写字,看起来就是 AI 做的 | 分镜阶段就禁止;情绪用符号(!、汗滴、爱心)表达 |
| 节奏太赶 | 所有事件一个速度堆在一起,观众没看懂就结束 | 分镜里写 reads 并给每个 read 时间;Base 指南说这是「模型最常出错的地方」 |
| 主角太小 | 大场景里一个小不点,全片看不清表演 | 中景 u≈20–28,特写 u≈40–70,副歌舞蹈约占画面高 40% |
| 表情硬切 | 前一帧哭后一帧笑 | 用 mood() / emotions() 做带预备动作的切换 |
| PDoom 在 macOS 上跑不起来 | Chrome 路径写死为 Windows;只传 --chrome 仍会报 Error creating webgl context,因为 --use-angle=d3d11 在 Mac 上不可用 | 把 render.mjs 里的 --use-angle=d3d11 改成 --use-angle=metal,再传 --chrome="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"(已实测);或直接用 Base 模板 |
--encode 出的视频没声音 | --encode 只认命令行 --audio=,不读 PROJECT.audio | 显式传 --audio=<音频路径>,或改用 --clip |
| 无 GPU / 核显 | 每帧数秒甚至更慢 | 让模型少用水彩 fill,改用平涂 wash |
十、实践建议
把这套方法迁移到自己的 MV,优先级是:
- 先把音乐数据做准:BPM、offset、歌词时间轴决定了所有卡点,这一步错了后面全部白做。
- 先有核心创意,再有镜头:PDoom 好看的根本原因是「失控的舞台剧」这个统一世界观,而不是某个镜头画得多精细。让模型先提 2–3 个核心创意,挑一个再展开分镜。
- 分镜是最便宜的修改点:在分镜阶段改一句话,比在代码阶段改一个镜头便宜两个数量级。分镜不满意就不要进入编码。
- 用 xhigh 推理 + 明确的美术约束:第一轮 Medium、无约束的结果是 Flash 风;约束(笔刷、少文字、转场、每镜有事件)加上高推理才有成片质量。
- 先做一段副歌验证:完整 MV 成本高,先拿 15–20 秒的一段副歌走通「分镜 → 实现 → 自查 → 出片」全链路,再铺开到全曲。
- 让模型「看」:不管是否并行,都要求每个镜头渲染联系表并查看。这是这套方法和「让模型盲写动画代码」之间的最大差别。
参考资料
- JohnHeibel — PDoomVideo · GitHub · 2026,成片见 YouTube↩
- JohnHeibel — ClaudeAnimationBase · GitHub · 2026↩